﻿using System;
using System.Drawing;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Forms;
using BGShell.Components;
using BGShell.PowerShell.LanguageService;
using BGShell.Interop;
using WindowsFormsAero;

namespace BGShell.PowerShell.Controls
{
    partial class ConsoleControl
    {
        internal sealed class InputReader : IDisposable
        {
            private static readonly string TabString = new string(' ', 4);

            private readonly PSLanguageService _langSvc;
            private readonly ConsoleControl _ctl;
            private readonly Object _syncRoot;
            private readonly Int32 _first;

            private CurrentLineInfo _line;
            private CommandHistory _history;

            public InputReader(ConsoleControl ctl)
                : this(ctl, null)
            {
            }

            public InputReader(ConsoleControl ctl, Object syncRoot)
            {
                _ctl = ctl;
                _syncRoot = syncRoot;
                _first = ctl.InvokeIfRequired(() => ctl.TextLength);

                if (IsCommand)
                {
                    _langSvc = new PSLanguageService(this, ctl);
                }

                _ctl.InvokeIfRequired(delegate
                {
                    _ctl.KeyDown += OnKeyDown;
                    _ctl.KeyPress += OnKeyPress;
                    _ctl.TextChanged += OnTextChanged;

                    _ctl.ProtectAll();
                    _ctl.Select(_first, 0);

                    using (_ctl.AllowProtectedUpdates())
                    {
                        _ctl.FormatSettings.Normal.Apply(_ctl.TextDocument.Selection.Font);
                    }

                    _ctl.CurrentStatus = ConsoleControlStatus.Ready;
                });
            }

            public void Dispose()
            {
                DisposeThis();
                DisposeLanguageService();
            }

            private void DisposeThis()
            {
                _ctl.KeyDown -= OnKeyDown;
                _ctl.KeyPress -= OnKeyPress;
                _ctl.TextChanged -= OnTextChanged;

                _ctl.ProtectAll();
                _ctl._reader = null;
            }

            private void DisposeLanguageService()
            {
                if (_langSvc != null)
                {
                    _langSvc.Dispose();
                }
            }

            public override string ToString()
            {
                return Text;
            }

            /// <remarks>Can be called from background threads.</remarks>
            public int GetRealCharIndex(int charIndex)
            {
                return _first + charIndex;
            }

            /// <remarks>Can be called from background threads.</remarks>
            public int GetFromRealCharIndex(int realCharIndex)
            {
                return realCharIndex - _first;
            }

            public RichTextRange GetRange(int first, int last)
            {
                return _ctl.TextDocument.GetRange(
                    GetRealCharIndex(first), 
                    GetRealCharIndex(last));
            }

            public bool CanCut
            {
                get { return IsCaretInEditArea && _ctl.SelectionLength > 0; }
            }

            public string Text
            {
                get
                {
                    if (_first >= _ctl.TextLength)
                    {
                        return string.Empty;
                    }

                    return _ctl.Text.Substring(_first);
                }
                set
                {
                    using (_ctl.AllowProtectedUpdates())
                    {
                        var format = _ctl.FormatSettings.Normal;
                        var range = _ctl.TextDocument.GetRange(_first, _ctl.TextLength);

                        range.Text = NormalizeLineBreaksAndTabs(value.TrimEnd());

                        _ctl.Select(range.End, 0);

                        format.Apply(range.Font);
                        format.Apply(_ctl.TextDocument.Selection.Font);
                    }

                    if (_langSvc != null)
                    {
                        _langSvc.Update(0, null, PSLanguageService.UpdateFlags.TextChanged);
                    }
                }
            }

            public int TextLength
            {
                get { return _ctl.TextLength - _first; }
            }

            public RichTextRange TextRange
            {
                get { return GetRange(0, TextLength); }
            }

            public CurrentLineInfo CurrentLine
            {
                get
                {
                    if (_line == null)
                    {
                        _line = new CurrentLineInfo(this);
                    }

                    return _line;
                }
            }

            public int SelectionStart
            {
                get { return GetFromRealCharIndex(_ctl.SelectionStart); }
            }

            public RichTextRange GetLineRange(int charIndex)
            {
                charIndex = GetRealCharIndex(charIndex);

                var lineIndex = _ctl.GetLineFromCharIndex(charIndex);
                var lineStart = _ctl.GetFirstCharIndexFromLine(lineIndex);
                var lineEnd = _ctl.GetFirstCharIndexFromLine(lineIndex + 1) - 1;

                if (lineStart < _first)
                {
                    lineStart = _first;
                }

                if (lineEnd < lineStart)
                {
                    lineEnd = _ctl.TextLength;
                }

                var range = _ctl.TextDocument.GetRange(lineStart, lineEnd);
                var text = range.Text;

                if (!string.IsNullOrEmpty(text))
                {
                    var whiteSpaceStart = text.Length - text.TrimStart().Length;
                    var whiteSpaceEnd = text.Length - text.TrimEnd().Length;

                    lineStart += whiteSpaceStart;
                    lineEnd -= whiteSpaceEnd;

                    if ((lineEnd - lineStart) > 0)
                    {
                        return _ctl.TextDocument.GetRange(lineStart, lineEnd);
                    }
                }

                return null;
            }

            public void Paste()
            {
                if (Clipboard.ContainsText())
                {
                    var str = Clipboard.GetText();

                    if (IsCaretInEditArea)
                    {
                        using (_ctl.AllowProtectedUpdates())
                        {
                            _ctl.SelectedText = NormalizeLineBreaksAndTabs(str);
                            _ctl.Select(_ctl.SelectionStart + _ctl.SelectionLength, 0);

                            UpdateLanguageService();
                        }
                    }
                    else
                    {
                        Text += str;
                    }
                }
            }

            internal bool ProcessDialogKey(Keys keyData)
            {
                var keyCode = keyData & Keys.KeyCode;

                var control = (keyData & Keys.Control) == Keys.Control;
                var shift = (keyData & Keys.Shift) == Keys.Shift;
                
                if (!control && IsCaretInEditArea && (keyCode == Keys.Tab))
                {
                    HandleTab(shift);
                    return true;
                }

                return false;
            }

            private void OnKeyDown(object sender, KeyEventArgs e)
            {
                HandleUpDown(e);
                HandleHome(e);
                HandleEscape(e);
                HandleEnter(e);
            }

            private void OnKeyPress(object sender, KeyPressEventArgs e)
            {
                if (!IsCaretInEditArea)
                {
                    _ctl.Select(_first, 0);
                }
            }

            private void OnTextChanged(object sender, EventArgs e)
            {
                if (_history != null)
                {
                    _history.UpdateEditedLine();
                }
            }

            private void HandleEnter(KeyEventArgs e)
            {
                if (e.KeyCode == Keys.Enter)
                {
                    e.Handled = true;

                    if (_ctl.IsBusy)
                    {
                        return;
                    }

                    if (e.Shift)
                    {
                        var selection =_ctl.TextDocument.Selection;
                        var indentStr = '\v' + new string(' ', CurrentLine.SpaceIndent);

                        using (_ctl.AllowProtectedUpdates())
                        {
                            selection.Text = indentStr + selection.Text;
                            selection.SetRange(selection.End, selection.End);
                        }

                        return;
                    }
                   
                    if (IsCommand)
                    {
                        _ctl.IsBusy = true;

                        if (_langSvc != null)
                        {
                            _langSvc.Update(0, ExecuteCommand, PSLanguageService.UpdateFlags.NoUpdateSelection);
                        }
                        else
                        {
                            ExecuteCommand();
                        }
                    }
                    else
                    {
                        Dispose();

                        lock (_syncRoot)
                        {
                            Monitor.Pulse(_syncRoot);
                        }
                    }
                }
            }

            private void ExecuteCommand()
            {
                if (_ctl.InvokeRequired)
                {
                    // must block the pipeline execution thread until the InputReader
                    // is successfully disposed of

                    _ctl.Invoke(new Action(ExecuteCommand));
                }
                else
                {
                    if (_langSvc.HasParseErrors)
                    {
                        _ctl.IsBusy = false;
                        _langSvc.ShowParseErrorBalloon();
                    }
                    else
                    {
                        Dispose();

                        _ctl.AppendText(Environment.NewLine);
                        _ctl.Pipeline = _ctl.Runspace.InvokeOutDefault(Text);
                    }
                }
            }

            private void HandleEscape(KeyEventArgs e)
            {
                if (e.KeyCode == Keys.Escape)
                {
                    if ((_langSvc != null) && (_langSvc.ParseErrorBalloonVisible))
                    {
                        _langSvc.HideParseErrorBalloon();
                    }
                    else if (_ctl.SelectionLength > 0)
                    {
                        _ctl.SelectionLength = 0;
                    }
                    else
                    {
                        Text = string.Empty;

                        if (_history != null)
                        {
                            _history.UpdateEditedLine();
                        }
                    }
                }
            }

            private void HandleHome(KeyEventArgs e)
            {
                if (!(e.Control) && (e.KeyCode == Keys.Home))
                {
                    var primaryHome = CurrentLine.FirstNonWhiteCharIndex;
                    var secondaryHome = CurrentLine.FirstCharIndex;

                    if (CurrentLine.Index == EditAreaFirstLine)
                    {
                        primaryHome = _first;
                    }

                    var selection = _ctl.TextDocument.Selection;
                    
                    var selectionStart = selection.Start;
                    var selectionEnd = selection.End;

                    if (selection.StartIsActive)
                    {
                        Swap(ref selectionStart, ref selectionEnd);
                    }

                    var selectedHome = primaryHome;

                    if (selectionEnd == primaryHome)
                    {
                        selectedHome = secondaryHome;
                    }

                    if (e.Shift)
                    {
                        if (selectedHome < selectionStart)
                        {
                            _ctl.Select(selectionStart, selectedHome - selectionEnd);
                        }
                        else
                        {
                            _ctl.Select(selectionEnd, selectionStart - selectedHome);
                        }
                    }
                    else
                    {
                        _ctl.Select(selectedHome, 0);
                    }

                    e.Handled = true;
                }
            }

            private void HandleUpDown(KeyEventArgs e)
            {
                if (!e.Shift && IsCaretInEditArea)
                {
                    var currentLine = _ctl.GetLineFromCharIndex(_ctl.SelectionStart);

                    if (e.KeyCode == Keys.Up)
                    {
                        var firstLine = _ctl.GetLineFromCharIndex(_first);

                        if (currentLine == firstLine)
                        {
                            History.Prev();
                            e.Handled = true;
                        }
                    }

                    if (e.KeyCode == Keys.Down)
                    {
                        var lastLine = _ctl.GetLineFromCharIndex(_ctl.TextLength);

                        if (currentLine == lastLine)
                        {
                            History.Next();
                            e.Handled = true;
                        }
                    }
                }
            }

            private void HandleTab(bool shift)
            {
                var sel = _ctl.SelectionStart;
                var selEmpty = _ctl.SelectionLength == 0;
                var beginOfLine = _ctl.GetFirstCharIndexOfCurrentLine();

                if (CurrentLine.Index == EditAreaFirstLine)
                {
                    beginOfLine = _first;
                }

                var selStartLine = _ctl.GetLineFromCharIndex(sel);
                var selEndLine = _ctl.GetLineFromCharIndex(sel + _ctl.SelectionLength);

                using (_ctl.AllowProtectedUpdates())
                {
                    if (selEmpty)
                    {
                        HandleTabSelectionEmpty(beginOfLine, sel, shift);
                    }
                    else if (selStartLine == selEndLine)
                    {
                        HandleTabSelectionSingleLine(beginOfLine, sel, shift);
                    }
                    else
                    {
                        HandleTabSelectionMoreLines(beginOfLine, sel, shift);
                    }
                }
            }

            private void HandleTabSelectionMoreLines(int beginOfStartLine, int sel, bool shift)
            {
                _ctl.Select(beginOfStartLine, _ctl.SelectionLength + (sel - beginOfStartLine));

                var str = _ctl.SelectedText;

                if (shift)
                {
                    if (!string.IsNullOrEmpty(str))
                    {
                        if (str.StartsWith(TabString, StringComparison.Ordinal))
                        {
                            str = str.Substring(TabString.Length);
                        }

                        str = str.Replace('\n' + TabString, "\v");
                        str = str.Replace('\n', '\v');
                    }
                }
                else
                {
                    str = TabString + _ctl.SelectedText;

                    if (str.EndsWith("\n", StringComparison.Ordinal))
                    {
                        str = str.Substring(0, str.Length - 1).Replace("\n", '\v' + TabString) + '\v';
                    }
                    else
                    {
                        str = str.Replace("\n", '\v' + TabString);
                    }
                }

                _ctl.SelectedText = str;
                _ctl.Select(beginOfStartLine, str.Length);

                UpdateLanguageService();
            }

            private void HandleTabSelectionSingleLine(int beginOfLine, int sel, bool shift)
            {
                if (shift)
                {
                    var precedingSpaces = GetPrecedingSpaces(beginOfLine, sel);

                    if (precedingSpaces != null)
                    {
                        precedingSpaces.Text = null;
                        _ctl.SelectionStart = precedingSpaces.End;
                    }
                }
                else
                {
                    _ctl.SelectedText = TabString;
                }

                UpdateLanguageService();
            }

            private void HandleTabSelectionEmpty(int beginOfLine, int sel, bool shift)
            {
                var precedingSpaces = GetPrecedingSpaces(beginOfLine, sel);

                if (!shift && ((precedingSpaces != null) || (beginOfLine == sel)))
                {
                    var range = _ctl.TextDocument.GetRange(sel, sel);

                    range.Text += TabString;
                    _ctl.SelectionStart = range.End;

                    UpdateLanguageService();
                }
                else if (shift && (precedingSpaces != null))
                {
                    precedingSpaces.Text = null;
                    _ctl.SelectionStart = precedingSpaces.End;

                    UpdateLanguageService();
                }
                else if (beginOfLine != sel)
                {
                    if (_langSvc != null)
                    {
                        _langSvc.TabExpansion(shift);
                    }
                }
            }

            private RichTextRange GetPrecedingSpaces(int beginOfLine, int sel)
            {
                if (beginOfLine == sel)
                {
                    return null;
                }

                var range = _ctl.TextDocument.GetRange(sel, sel);
                var moved = range.MoveStartWhile(-TabString.Length, ' ');

                if (moved < 0)
                {
                    return range;
                }

                return null;
            }

            private void UpdateLanguageService()
            {
                if (_langSvc != null)
                {
                    _langSvc.Update(0, null, PSLanguageService.UpdateFlags.TextChanged);
                }
            }

            private bool IsCaretInEditArea
            {
                get
                { 
                    return _ctl.SelectionStart >= _first && 
                           _ctl.SelectionLength >= 0; }
            }

            private bool IsCommand
            {
                get { return _syncRoot == null; }
            }

            private int EditAreaFirstLine
            {
                get { return _ctl.GetLineFromCharIndex(_first); }
            }

            private CommandHistory History
            {
                get
                {
                    if (_history == null)
                    {
                        _history = new CommandHistory(_ctl, this);
                    }

                    return _history;
                }
            }

            private static void Swap<T>(ref T x, ref T y)
            {
                T tmp = x;
                x = y;
                y = tmp;
            }

            private static string NormalizeLineBreaksAndTabs(string s)
            {
                if (!string.IsNullOrEmpty(s))
                {
                    return TabAndLineBreakRegex.Replace(s, m => (m.Value == "\t") ? TabString : "\v");
                }

                return s;
            }

            private static readonly Regex TabAndLineBreakRegex = new Regex(@"\r\n|\r|\n|\t", RegexOptions.Compiled);

            public sealed class CurrentLineInfo
            {
                private readonly ConsoleControl _ctl;

                public CurrentLineInfo(InputReader reader)
                {
                    _ctl = reader._ctl;
                }

                public int Index
                {
                    get { return _ctl.GetLineFromCharIndex(FirstCharIndex); }
                }

                /// <remarks>This is a real char index, not an offset from the start of edit area</remarks>
                public int FirstCharIndex
                {
                    get { return _ctl.GetFirstCharIndexOfCurrentLine(); }
                }

                /// <remarks>This is a real char index, not an offset from the start of edit area</remarks>
                public int LastCharIndex
                {
                    get
                    {
                        var line = _ctl.GetLineFromCharIndex(FirstCharIndex);
                        var ch = _ctl.GetFirstCharIndexFromLine(line + 1);

                        if (ch < 0)
                        {
                            return _ctl.TextLength;
                        }

                        return ch - 1;
                    }
                }

                /// <remarks>This is a real char index, not an offset from the start of edit area</remarks>
                public int FirstNonWhiteCharIndex
                {
                    get
                    {
                        var text = _ctl.Text;
                        var index = FirstCharIndex;

                        while(index < text.Length)
                        {
                            var ch = text[index];

                            if ((ch == '\r') ||
                                (ch == '\n') ||
                                (ch == '\v') ||
                                (!char.IsWhiteSpace(ch)))
                            {
                                break;
                            }

                            index++;
                        }

                        return index;
                    }
                }

                public int SpaceIndent
                {
                    get { return FirstNonWhiteCharIndex - FirstCharIndex; }
                }
            }
        }
    }
}